/*
 * JavaScript TimeSpan Library
 *
 * Copyright (c) 2010 Michael Stum, Charlie Robbins
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

//
// ### Time constants
//
var msecPerSecond = 1000,
  msecPerMinute = 60000,
  msecPerHour = 3600000,
  msecPerDay = 86400000;

//
// ### Timespan Parsers
//
var timeSpanWithDays = /^(\d+):(\d+):(\d+):(\d+)(\.\d+)?/,
  timeSpanNoDays = /^(\d+):(\d+):(\d+)(\.\d+)?/;
const errNone: any = null;
//
// ### @private function isNumeric (input)
// #### @input {Number} Value to check numeric quality of.
// Returns a value indicating the numeric quality of the
// specified `input`.
//
function isNumeric(input: any): boolean {
  return input && !isNaN(parseFloat(input)) && isFinite(input);
}
export default class TimeSpan {
  msecs: number = 0;
  //
  // ### function TimeSpan (milliseconds, seconds, minutes, hours, days)
  // #### @milliseconds {Number} Number of milliseconds for this instance.
  // #### @seconds {Number} Number of seconds for this instance.
  // #### @minutes {Number} Number of minutes for this instance.
  // #### @hours {Number} Number of hours for this instance.
  // #### @days {Number} Number of days for this instance.
  // Constructor function for the `TimeSpan` object which represents a length
  // of positive or negative milliseconds componentized into milliseconds,
  // seconds, hours, and days.
  //
  // var TimeSpan = (exports.TimeSpan = function(
  public constructor(
    milliseconds: number,
    seconds: number,
    minutes: number,
    hours: number,
    days: number
  ) {
    this.msecs = 0;

    if (isNumeric(days)) {
      this.msecs += days * msecPerDay;
    }

    if (isNumeric(hours)) {
      this.msecs += hours * msecPerHour;
    }

    if (isNumeric(minutes)) {
      this.msecs += minutes * msecPerMinute;
    }

    if (isNumeric(seconds)) {
      this.msecs += seconds * msecPerSecond;
    }

    if (isNumeric(milliseconds)) {
      this.msecs += milliseconds;
    }
  }

  //
  // ## Factory methods
  // Helper methods for creating new TimeSpan objects
  // from various criteria: milliseconds, seconds, minutes,
  // hours, days, strings and other `TimeSpan` instances.
  //

  //
  // ### function fromMilliseconds (milliseconds)
  // #### @milliseconds {Number} Amount of milliseconds for the new TimeSpan instance.
  // Creates a new `TimeSpan` instance with the specified `milliseconds`.
  //
  // exports.fromMilliseconds = function(milliseconds) {
  public static fromMilliseconds(milliseconds: number): TimeSpan {
    if (!isNumeric(milliseconds)) {
      return errNone;
    }
    return new TimeSpan(milliseconds, 0, 0, 0, 0);
  }

  //
  // ### function fromSeconds (seconds)
  // #### @milliseconds {Number} Amount of seconds for the new TimeSpan instance.
  // Creates a new `TimeSpan` instance with the specified `seconds`.
  //
  // exports.fromSeconds = function(seconds) {
  public static fromSeconds(seconds: number): TimeSpan {
    if (!isNumeric(seconds)) {
      return errNone;
    }
    return new TimeSpan(0, seconds, 0, 0, 0);
  }

  //
  // ### function fromMinutes (milliseconds)
  // #### @milliseconds {Number} Amount of minutes for the new TimeSpan instance.
  // Creates a new `TimeSpan` instance with the specified `minutes`.
  //
  // exports.fromMinutes = function(minutes) {
  public static fromMinutes(minutes: number): TimeSpan {
    if (!isNumeric(minutes)) {
      return errNone;
    }
    return new TimeSpan(0, 0, minutes, 0, 0);
  }

  //
  // ### function fromHours (hours)
  // #### @milliseconds {Number} Amount of hours for the new TimeSpan instance.
  // Creates a new `TimeSpan` instance with the specified `hours`.
  //
  // exports.fromHours = function(hours) {
  public static fromHours(hours: number): TimeSpan {
    if (!isNumeric(hours)) {
      return errNone;
    }
    return new TimeSpan(0, 0, 0, hours, 0);
  }

  //
  // ### function fromDays (days)
  // #### @milliseconds {Number} Amount of days for the new TimeSpan instance.
  // Creates a new `TimeSpan` instance with the specified `days`.
  //
  // exports.fromDays = function(days) {
  public static fromDays(days: number): TimeSpan {
    if (!isNumeric(days)) {
      return errNone;
    }
    return new TimeSpan(0, 0, 0, 0, days);
  }

  private static obj2Number(x: any): number {
    if (x instanceof String) {
      return parseFloat(x as string);
    }
    if (x instanceof Number) {
      return x as number;
    }
    return 0;
  }
  //
  // ### function parse (str)
  // #### @strData {string} Timespan string to parse.
  // Creates a new `TimeSpan` instance from the specified
  // string, `strData`.
  //
  // exports.parse = function(str) {
  public static parse(strData: string): TimeSpan {
    var match, milliseconds;

    function parseMilliseconds(value: any) {
      return value ? parseFloat("0" + value) * 1000 : 0;
    }
    // If we match against a full TimeSpan:
    //   [days]:[hours]:[minutes]:[seconds].[milliseconds]?
    if ((match = strData.match(timeSpanWithDays))) {
      return new TimeSpan(
        parseMilliseconds(match[5]),
        TimeSpan.obj2Number(match[4]),
        TimeSpan.obj2Number(match[3]),
        TimeSpan.obj2Number(match[2]),
        TimeSpan.obj2Number(match[1])
      );
    }

    // If we match against a partial TimeSpan:
    //   [hours]:[minutes]:[seconds].[milliseconds]?
    if ((match = strData.match(timeSpanNoDays))) {
      return new TimeSpan(
        parseMilliseconds(match[4]),
        TimeSpan.obj2Number(match[3]),
        TimeSpan.obj2Number(match[2]),
        TimeSpan.obj2Number(match[1]),
        0
      );
    }

    return errNone;
  }

  //
  // List of default singular time modifiers and associated
  // computation algoritm. Assumes in order, smallest to greatest
  // performing carry forward additiona / subtraction for each
  // Date-Time component.
  //
  private static parsers: any = {
    milliseconds: {
      exp: /(\d+)milli(?:second)?[s]?/i,
      compute: function(delta: any, computed: any) {
        return TimeSpan._compute(delta, computed, {
          current: "milliseconds",
          next: "seconds",
          max: 1000
        });
      }
    },
    seconds: {
      exp: /(\d+)second[s]?/i,
      compute: function(delta: any, computed: any) {
        return TimeSpan._compute(delta, computed, {
          current: "seconds",
          next: "minutes",
          max: 60
        });
      }
    },
    minutes: {
      exp: /(\d+)minute[s]?/i,
      compute: function(delta: any, computed: any) {
        return TimeSpan._compute(delta, computed, {
          current: "minutes",
          next: "hours",
          max: 60
        });
      }
    },
    hours: {
      exp: /(\d+)hour[s]?/i,
      compute: function(delta: any, computed: any) {
        return TimeSpan._compute(delta, computed, {
          current: "hours",
          next: "days",
          max: 24
        });
      }
    },
    days: {
      exp: /(\d+)day[s]?/i,
      compute: function(delta: any, computed: any) {
        var days = TimeSpan.monthDays(computed.months, computed.years),
          sign = delta >= 0 ? 1 : -1,
          opsign = delta >= 0 ? -1 : 1,
          clean = 0;

        function update(months: any) {
          if (months < 0) {
            computed.years -= 1;
            return 11;
          } else if (months > 11) {
            computed.years += 1;
            return 0;
          }

          return months;
        }

        if (delta) {
          while (Math.abs(delta) >= days) {
            computed.months += sign * 1;
            computed.months = update(computed.months);
            delta += opsign * days;
            days = TimeSpan.monthDays(computed.months, computed.years);
          }

          computed.days += opsign * delta;
        }

        if (computed.days < 0) {
          clean = -1;
        } else if (computed.days > TimeSpan.months[computed.months]) {
          clean = 1;
        }

        if (clean === -1 || clean === 1) {
          computed.months += clean;
          computed.months = update(computed.months);
          computed.days = TimeSpan.months[computed.months] + computed.days;
        }

        return computed;
      }
    },
    months: {
      exp: /(\d+)month[s]?/i,
      compute: function(delta: any, computed: any) {
        var round = delta > 0 ? Math.floor : Math.ceil;
        if (delta) {
          computed.years += round.call(null, delta / 12);
          computed.months += delta % 12;
        }

        if (computed.months > 11) {
          computed.years += Math.floor((computed.months + 1) / 12);
          computed.months = ((computed.months + 1) % 12) - 1;
        }

        return computed;
      }
    },
    years: {
      exp: /(\d+)year[s]?/i,
      compute: function(delta: any, computed: any) {
        if (delta) {
          computed.years += delta;
        }
        return computed;
      }
    }
  };

  //
  // Compute the list of parser names for
  // later use.
  //
  private static parserNames = Object.keys(TimeSpan.parsers);

  //
  // ### function parseDate (str)
  // #### @str {string} String to parse into a date
  // Parses the specified liberal Date-Time string according to
  // ISO8601 **and**:
  //
  // 1. `2010-04-03T12:34:15Z+12MINUTES`
  // 2. `NOW-4HOURS`
  //
  // Valid modifiers for the more liberal Date-Time string(s):
  //
  //     YEAR, YEARS
  //     MONTH, MONTHS
  //     DAY, DAYS
  //     HOUR, HOURS
  //     MINUTE, MINUTES
  //     SECOND, SECONDS
  //     MILLI, MILLIS, MILLISECOND, MILLISECONDS
  //
  // exports.parseDate = function(str) {
  public static parseDate(str: string): Date {
    var dateTime = Date.parse(str);
    let iso = "^([^Z]+)";
    let zulu = "Z([\\+|\\-])?";
    let diff: any = {};
    // var computed: any;
    var modifiers;
    var sign: any;
    let errDate: any = null;
    //
    // If Date string supplied actually conforms
    // to UTC Time (ISO8601), return a new Date.
    //
    if (!isNaN(dateTime)) {
      return new Date(dateTime);
    }

    //
    // Create the `RegExp` for the end component
    // of the target `str` to parse.
    //
    TimeSpan.parserNames.forEach(function(group) {
      zulu += "(\\d+[a-zA-Z]+)?";
    });

    if (/^NOW/i.test(str)) {
      //
      // If the target `str` is a liberal `NOW-*`,
      // then set the base `dateTime` appropriately.
      //
      dateTime = Date.now();
      zulu = zulu.replace(/Z/, "NOW");
    } else if (/^-/.test(str) || /^\+/.test(str)) {
      dateTime = Date.now();
      zulu = zulu.replace(/Z/, "");
    } else {
      //
      // Parse the `ISO8601` component, and the end
      // component from the target `str`.
      //
      let xdateTime = str.match(new RegExp(iso, "i"));
      if (xdateTime) {
        dateTime = Date.parse(xdateTime[1]);
      }
    }

    //
    // If there was no match on either part then
    // it must be a bad value.
    //
    if (!dateTime || !(modifiers = str.match(new RegExp(zulu, "i")))) {
      return errDate;
    }

    //
    // Create a new `Date` object from the `ISO8601`
    // component of the target `str`.
    //
    let rdateTime = new Date(dateTime);
    sign = modifiers[1] === "+" ? 1 : -1;

    //
    // Create an Object-literal for consistently accessing
    // the various components of the computed Date.
    //
    var computed: any = {
      milliseconds: rdateTime.getMilliseconds(),
      seconds: rdateTime.getSeconds(),
      minutes: rdateTime.getMinutes(),
      hours: rdateTime.getHours(),
      days: rdateTime.getDate(),
      months: rdateTime.getMonth(),
      years: rdateTime.getFullYear()
    };

    //
    // Parse the individual component spans (months, years, etc)
    // from the modifier strings that we parsed from the end
    // of the target `str`.
    //
    modifiers
      .slice(2)
      .filter(Boolean)
      .forEach(function(modifier) {
        TimeSpan.parserNames.forEach(function(name) {
          var match;
          if (!(match = modifier.match(TimeSpan.parsers[name].exp))) {
            return errDate;
          }
          diff[name] = sign * parseInt(match[1], 10);
        });
      });

    //
    // Compute the total `diff` by iteratively computing
    // the partial components from smallest to largest.
    //
    TimeSpan.parserNames.forEach(function(name) {
      computed = TimeSpan.parsers[name].compute(diff[name], computed);
    });

    return new Date(
      computed.years,
      computed.months,
      computed.days,
      computed.hours,
      computed.minutes,
      computed.seconds,
      computed.milliseconds
    );
  }

  //
  // ### function fromDates (start, end, abs)
  // #### @start {Date} Start date of the `TimeSpan` instance to return
  // #### @end {Date} End date of the `TimeSpan` instance to return
  // #### @abs {boolean} Value indicating to return an absolute value
  // Returns a new `TimeSpan` instance representing the difference between
  // the `start` and `end` Dates.
  //
  // exports.fromDates = function(start, end, abs) {
  public static fromDates(
    start: any,
    end: any,
    abs: boolean = false
  ): TimeSpan {
    if (typeof start === "string") {
      start = TimeSpan.parseDate(start);
    }

    if (typeof end === "string") {
      end = TimeSpan.parseDate(end);
    }

    if (!(start instanceof Date && end instanceof Date)) {
      return errNone;
    }

    var differenceMsecs = end.valueOf() - start.valueOf();
    if (abs) {
      differenceMsecs = Math.abs(differenceMsecs);
    }

    return new TimeSpan(differenceMsecs, 0, 0, 0, 0);
  }

  //
  // ## Module Helpers
  // Module-level helpers for various utilities such as:
  // instanceOf, parsability, and cloning.
  //

  //
  // ### function test (str)
  // #### @str {string} String value to test if it is a TimeSpan
  // Returns a value indicating if the specified string, `str`,
  // is a parsable `TimeSpan` value.
  //
  // exports.test = function(str) {
  public static test(str: string): boolean {
    return timeSpanWithDays.test(str) || timeSpanNoDays.test(str);
  }

  //
  // ### function instanceOf (timeSpan)
  // #### @timeSpan {Object} Object to check TimeSpan quality.
  // Returns a value indicating if the specified `timeSpan` is
  // in fact a `TimeSpan` instance.
  //
  // exports.instanceOf = function(timeSpan) {
  public static instanceOf(timeSpan: any): boolean {
    return timeSpan instanceof TimeSpan;
  }

  //
  // ### function clone (timeSpan)
  // #### @timeSpan {TimeSpan} TimeSpan object to clone.
  // Returns a new `TimeSpan` instance with the same value
  // as the `timeSpan` object supplied.
  //
  // exports.clone = function(timeSpan) {
  public static clone(timeSpan: TimeSpan): TimeSpan {
    if (!(timeSpan instanceof TimeSpan)) {
      return errNone;
    }
    return TimeSpan.fromMilliseconds(timeSpan.totalMilliseconds());
  }

  //
  // ## Addition
  // Methods for adding `TimeSpan` instances,
  // milliseconds, seconds, hours, and days to other
  // `TimeSpan` instances.
  //

  //
  // ### function add (timeSpan)
  // #### @timeSpan {TimeSpan} TimeSpan to add to this instance
  // Adds the specified `timeSpan` to this instance.
  //
  // TimeSpan.prototype.add = function(timeSpan) {
  public add(timeSpan: TimeSpan) {
    if (!(timeSpan instanceof TimeSpan)) {
      return;
    }
    this.msecs += timeSpan.totalMilliseconds();
  }

  //
  // ### function addMilliseconds (milliseconds)
  // #### @milliseconds {Number} Number of milliseconds to add.
  // Adds the specified `milliseconds` to this instance.
  //
  // TimeSpan.prototype.addMilliseconds = function(milliseconds) {
  public addMilliseconds(milliseconds: number) {
    if (!isNumeric(milliseconds)) {
      return;
    }
    this.msecs += milliseconds;
  }

  //
  // ### function addSeconds (seconds)
  // #### @seconds {Number} Number of seconds to add.
  // Adds the specified `seconds` to this instance.
  //
  // TimeSpan.prototype.addSeconds = function(seconds) {
  public addSeconds(seconds: number) {
    if (!isNumeric(seconds)) {
      return;
    }

    this.msecs += seconds * msecPerSecond;
  }

  //
  // ### function addMinutes (minutes)
  // #### @minutes {Number} Number of minutes to add.
  // Adds the specified `minutes` to this instance.
  //
  // TimeSpan.prototype.addMinutes = function(minutes) {
  public addMinutes(minutes: number) {
    if (!isNumeric(minutes)) {
      return;
    }
    this.msecs += minutes * msecPerMinute;
  }

  //
  // ### function addHours (hours)
  // #### @hours {Number} Number of hours to add.
  // Adds the specified `hours` to this instance.
  //
  // TimeSpan.prototype.addHours = function(hours) {
  public addHours(hours: number) {
    if (!isNumeric(hours)) {
      return;
    }
    this.msecs += hours * msecPerHour;
  }

  //
  // ### function addDays (days)
  // #### @days {Number} Number of days to add.
  // Adds the specified `days` to this instance.
  //
  //TimeSpan.prototype.addDays = function(days) {
  public addDays(days: number) {
    if (!isNumeric(days)) {
      return;
    }
    this.msecs += days * msecPerDay;
  }

  //
  // ## Subtraction
  // Methods for subtracting `TimeSpan` instances,
  // milliseconds, seconds, hours, and days from other
  // `TimeSpan` instances.
  //

  //
  // ### function subtract (timeSpan)
  // #### @timeSpan {TimeSpan} TimeSpan to subtract from this instance.
  // Subtracts the specified `timeSpan` from this instance.
  //
  // TimeSpan.prototype.subtract = function(timeSpan) {
  public subtract(timeSpan: TimeSpan) {
    if (!(timeSpan instanceof TimeSpan)) {
      return;
    }
    this.msecs -= timeSpan.totalMilliseconds();
  }

  //
  // ### function subtractMilliseconds (milliseconds)
  // #### @milliseconds {Number} Number of milliseconds to subtract.
  // Subtracts the specified `milliseconds` from this instance.
  //
  // TimeSpan.prototype.subtractMilliseconds = function(milliseconds) {
  public subtractMilliseconds(milliseconds: number) {
    if (!isNumeric(milliseconds)) {
      return;
    }
    this.msecs -= milliseconds;
  }

  //
  // ### function subtractSeconds (seconds)
  // #### @seconds {Number} Number of seconds to subtract.
  // Subtracts the specified `seconds` from this instance.
  //
  // TimeSpan.prototype.subtractSeconds = function(seconds) {
  public subtractSeconds(seconds: number) {
    if (!isNumeric(seconds)) {
      return;
    }
    this.msecs -= seconds * msecPerSecond;
  }

  //
  // ### function subtractMinutes (minutes)
  // #### @minutes {Number} Number of minutes to subtract.
  // Subtracts the specified `minutes` from this instance.
  //
  // TimeSpan.prototype.subtractMinutes = function(minutes) {
  public subtractMinutes(minutes: number) {
    if (!isNumeric(minutes)) {
      return;
    }
    this.msecs -= minutes * msecPerMinute;
  }

  //
  // ### function subtractHours (hours)
  // #### @hours {Number} Number of hours to subtract.
  // Subtracts the specified `hours` from this instance.
  //
  // TimeSpan.prototype.subtractHours = function(hours) {
  public subtractHours(hours: number) {
    if (!isNumeric(hours)) {
      return;
    }
    this.msecs -= hours * msecPerHour;
  }

  //
  // ### function subtractDays (days)
  // #### @days {Number} Number of days to subtract.
  // Subtracts the specified `days` from this instance.
  //
  // TimeSpan.prototype.subtractDays = function(days) {
  public subtractDays(days: number) {
    if (!isNumeric(days)) {
      return;
    }
    this.msecs -= days * msecPerDay;
  }

  //
  // ## Getters
  // Methods for retrieving components of a `TimeSpan`
  // instance: milliseconds, seconds, minutes, hours, and days.
  //

  //
  // ### function totalMilliseconds (roundDown)
  // #### @roundDown {boolean} Value indicating if the value should be rounded down.
  // Returns the total number of milliseconds for this instance, rounding down
  // to the nearest integer if `roundDown` is set.
  //
  // TimeSpan.prototype.totalMilliseconds = function(roundDown) {
  public totalMilliseconds(roundDown: boolean = true) {
    var result = this.msecs;
    if (roundDown === true) {
      result = Math.floor(result);
    }

    return result;
  }

  //
  // ### function totalSeconds (roundDown)
  // #### @roundDown {boolean} Value indicating if the value should be rounded down.
  // Returns the total number of seconds for this instance, rounding down
  // to the nearest integer if `roundDown` is set.
  //
  // TimeSpan.prototype.totalSeconds = function(roundDown) {
  public totalSeconds(roundDown: boolean = true) {
    var result = this.msecs / msecPerSecond;
    if (roundDown === true) {
      result = Math.floor(result);
    }

    return result;
  }

  //
  // ### function totalMinutes (roundDown)
  // #### @roundDown {boolean} Value indicating if the value should be rounded down.
  // Returns the total number of minutes for this instance, rounding down
  // to the nearest integer if `roundDown` is set.
  //
  // TimeSpan.prototype.totalMinutes = function(roundDown) {
  public totalMinutes(roundDown: boolean = true) {
    var result = this.msecs / msecPerMinute;
    if (roundDown === true) {
      result = Math.floor(result);
    }

    return result;
  }

  //
  // ### function totalHours (roundDown)
  // #### @roundDown {boolean} Value indicating if the value should be rounded down.
  // Returns the total number of hours for this instance, rounding down
  // to the nearest integer if `roundDown` is set.
  //
  // TimeSpan.prototype.totalHours = function(roundDown) {
  public totalHours(roundDown: boolean = true) {
    var result = this.msecs / msecPerHour;
    if (roundDown === true) {
      result = Math.floor(result);
    }

    return result;
  }

  //
  // ### function totalDays (roundDown)
  // #### @roundDown {boolean} Value indicating if the value should be rounded down.
  // Returns the total number of days for this instance, rounding down
  // to the nearest integer if `roundDown` is set.
  //
  //TimeSpan.prototype.totalDays = function(roundDown) {
  public totalDays(roundDown: boolean = true) {
    var result = this.msecs / msecPerDay;
    if (roundDown === true) {
      result = Math.floor(result);
    }

    return result;
  }

  //
  // ### @milliseconds
  // Returns the length of this `TimeSpan` instance in milliseconds.
  //
  // TimeSpan.prototype.__defineGetter__("milliseconds", function() {
  //   return this.msecs % 1000;
  // });
  public get milliseconds(): number {
    return this.msecs % 1000;
  }

  //
  // ### @seconds
  // Returns the length of this `TimeSpan` instance in seconds.
  //
  // TimeSpan.prototype.__defineGetter__("seconds", function() {
  //   return Math.floor(this.msecs / msecPerSecond) % 60;
  // });
  public get seconds(): number {
    return Math.floor(this.msecs / msecPerSecond) % 60;
  }

  //
  // ### @minutes
  // Returns the length of this `TimeSpan` instance in minutes.
  //
  // TimeSpan.prototype.__defineGetter__("minutes", function() {
  // return Math.floor(this.msecs / msecPerMinute) % 60;
  // });
  public get minutes(): number {
    return Math.floor(this.msecs / msecPerMinute) % 60;
  }
  //
  // ### @hours
  // Returns the length of this `TimeSpan` instance in hours.
  //
  // TimeSpan.prototype.__defineGetter__("hours", function() {
  //   return Math.floor(this.msecs / msecPerHour) % 24;
  // });
  public get hours(): number {
    return Math.floor(this.msecs / msecPerHour) % 24;
  }

  //
  // ### @days
  // Returns the length of this `TimeSpan` instance in days.
  //
  // TimeSpan.prototype.__defineGetter__("days", function() {
  //   return Math.floor(this.msecs / msecPerDay);
  // });
  public get days(): number {
    return Math.floor(this.msecs / msecPerDay);
  }

  //
  // ## Instance Helpers
  // Various help methods for performing utilities
  // such as equality and serialization
  //

  //
  // ### function equals (timeSpan)
  // #### @timeSpan {TimeSpan} TimeSpan instance to assert equal
  // Returns a value indicating if the specified `timeSpan` is equal
  // in milliseconds to this instance.
  //
  // TimeSpan.prototype.equals = function(timeSpan) {
  public equals(timeSpan: TimeSpan): boolean {
    if (!(timeSpan instanceof TimeSpan)) {
      return false;
    }
    return this.msecs === timeSpan.totalMilliseconds();
  }

  //
  // ### function toString ()
  // Returns a string representation of this `TimeSpan`
  // instance according to current `format`.
  //
  // TimeSpan.prototype.toString = function() {
  public toString(): string {
    // if (!this.format) {
    return this._format();
    // }
    // return this.format(this);
  }

  //
  // ### @private function _format ()
  // Returns the default string representation of this instance.
  //
  // TimeSpan.prototype._format = function() {
  private _format(): string {
    return [
      this.days,
      this.hours,
      this.minutes,
      this.seconds + "." + this.milliseconds
    ].join(":");
  }

  //
  // ### @private function _compute (delta, date, computed, options)
  // #### @delta {Number} Channge in this component of the date
  // #### @computed {Object} Currently computed date.
  // #### @options {Object} Options for the computation
  // Performs carry forward addition or subtraction for the
  // `options.current` component of the `computed` date, carrying
  // it forward to `options.next` depending on the maximum value,
  // `options.max`.
  //
  private static _compute(delta: number, computed: any, options: any) {
    var current = options.current,
      next = options.next,
      max = options.max,
      round = delta > 0 ? Math.floor : Math.ceil;

    if (delta) {
      computed[next] += round.call(null, delta / max);
      computed[current] += delta % max;
    }

    if (Math.abs(computed[current]) >= max) {
      computed[next] += round.call(null, computed[current] / max);
      computed[current] = computed[current] % max;
    }

    return computed;
  }

  //
  // ### @private monthDays (month, year)
  // #### @month {Number} Month to get days for.
  // #### @year {Number} Year of the month to get days for.
  // Returns the number of days in the specified `month` observing
  // leap years.
  //
  private static months: number[] = [
    31,
    28,
    31,
    30,
    31,
    30,
    31,
    31,
    30,
    31,
    30,
    31
  ];
  public static monthDays(month: number, year: number): number {
    if (
      ((year % 100 !== 0 && year % 4 === 0) || year % 400 === 0) &&
      month === 1
    ) {
      return 29;
    }

    return this.months[month];
  }
}
